SERVEI DE PERSISTÈNCIA







Introducció

Propòsit

El Servei de Persistència permet persistir i recuperar dades entre l'aplicatiu i el motor de base de dades. La base tecnològica està basada en Spring i Hibernate, i entre d'altres característiques ofereix:

  1. Suport per a DAO (Data Access Object) orientat principalment a facilitar la feina amb les tecnologies d'accés a dades.
  2. Traducció d'excepcions específiques de la tecnologia utilitzada a una jerarquia d'excepcions genérica
  3. Transaccionalitat declarativa

Context i Escenaris d'Ús

El Servei de Persistència es troba dins dels serveis de Propòsit General de Canigó.

Versions i Dependències

En el present apartat es mostren quines són les versions i dependències necessàries per fer ús del Servei.

Dependències Bàsiques

Les dependències descrites a la següent url són requerides per tal de compilar i fer funcionar el projecte:
Dependències Servei de Persistència

Cal destacar que dintre de la dependència "Hibernate" s'inclouen totes les llibreries necessàries per al funcionament correcte d'aquesta capa de persistència. Per a veure quines són es pot consultar la web http://www.hibernate.org

A qui va dirigit

Aquest document va dirigit als següents perfils:

  1. Programador. Per conéixer l'ús del servei
  2. Arquitecte. Per conéixer quins són els components i la configuració del servei
  3. Administrador. Per conéixer com configurar el servei en cadascun dels entorns en cas de necessitat

Documents i Fonts de Referència

[1] Hibernate http://www.hibernate.org

Glossari

DAO
Data Access Object, permet persistir les dades d'un objecte Java (normalment un POJO) a una base de dades

POJO
Plain Old Java Object, nom que se li dóna a les classes Java normals que no són de cap tipus especial (EJB's, etc) i que segueixen la convenció JavaBean.

BO
Business Object, objecte que implementa la lògica de negoci.

VO
Value Object, objecte que fa de transport de les dades necessàries per a la lógica de negoci.

Descripció Detallada

Arquitectura i Components

Existeixen tres tipus de components. Podem classificar-los en:

  1. Interfícies i components genèrics.
  2. Implementació de les interfícies.

Es pot trobar tota la documentació JavaDoc i el codi font referent aquests components a les següents urls:

JavaDoc: http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-persistence/apidocs/index.html
Codi Font:  http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-persistence/xref/index.html

 Instal.lació i Configuració

Instal.lació

La instal.lació del servei requereix de la utilització de la llibreria 'canigo-services-persistence' i les dependències indicades a l'apartat 'Introducció-Versions i Dependències'.

Configuració

La configuració del servei implica:

  1. Definir el servei i injectar-li les seves dependències
  2. Enllaç d'una acció amb el seu "DAO"

Definició del servei i de les seves dependències


Fitxer de configuració: canigo-services-persistence.xml
Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring
Factoria de sessions d'Hibernate

Atributs:

Atribut Requerit Descripció
class Implementació de la factoria de sessions

Opcions:
# org.springframework.orm.hibernate3.LocalSessionFactoryBean

També és necessari configurar les següents propietats:

Propietat Requerit Descripció
configLocation Ubicació del fitxer de propietats d'Hibernate
hibernateProperties No Propietats d'Hibernate opcionals per a sobreescriure en funció del host

Exemple:

...

<bean  id="sessionFactory"

class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">

<property  name="configLocation"

value="classpath:$\{sessionFactory.configLocation\}"  />

<property  name="hibernateProperties">

<props>

<prop  key="hibernate.connection.datasource">$\{dataSource.jndiName\}</prop>

</props>

</property>

</bean>

...








Fitxer de configuració: canigo-services-persistence.xml
Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring

Delegat transaccional
Atributs:
|| Atribut || Requerit || Descripció ||

class Implementació del delegat transaccional

Opcions:
# org.springframework.orm.hibernate3.HibernateTransactionManager

També és necessari configurar les següents propietats:

Propietat Requerit Descripció
sessionFactory Referència a la factoria de sessions

Exemple:

...

<!-- Transaction manager for a single Hibernate SessionFactory  -->

<bean  id="transactionManager"

class="org.springframework.orm.hibernate3.HibernateTransactionManager">

<property  name="sessionFactory" ref="sessionFactory" />

</bean>

...







Fitxer de configuració: canigo-services-persistence.xml
Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring
Proxy per a la definició declarativa de la transaccionalitat dels nostres DAO's. Servirà de bean "pare" d'altres beans.
Atributs:
|| Atribut || Requerit || Descripció ||

class Implementació proxy

Opcions:
# org.springframework.transaction.interceptor.TransactionProxyFactoryBean

També és necessari configurar les següents propietats:

Propietat Requerit Descripció
target Implementació del DAO sobre la que actuarà el proxy. Com estem en la definició "pare" del proxy, el valor serà "java.lang.Object"
transactionAttributes Si Atributs transaccionals dels diferents mètodes dels DAO's. Per a més informació sobre les diferents opcions consultar
http://www.springframework.org/docs/api/index.html?org/springframework/ transaction/interceptor/TransactionProxyFactoryBean.html

Exemple:

...

<bean  id="baseDaoProxy"

class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">

<property  name="transactionManager">

<ref local="transactionManager"  />

</property>

<property name="target">

<bean  class="java.lang.Object" />

</property>

<property  name="transactionAttributes">

<props>

<prop  key="get*">PROPAGATION_REQUIRED,readOnly</prop>

<prop  key="find*">PROPAGATION_REQUIRED,readOnly</prop>

<prop  key="load*">PROPAGATION_REQUIRED,readOnly</prop>

<prop  key="store*">PROPAGATION_REQUIRED</prop>

<prop  key="save*">PROPAGATION_REQUIRED</prop>

<prop  key="delete*">PROPAGATION_REQUIRED</prop>

</props>

</property>

</bean>

...



Enllaç d'una acció amb el seu DAO




Fitxer de configuració: action-servlet-XXX.xml
Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring

Cada acció que implementi una búsqueda necessita la definició d'un bean que hereti del bean "pare" del servei i que tingui els següents atributs:
Atributs:
|| Atribut || Requerit || Descripció ||

parent Bean "pare". Opcions:

# baseDaoProxy

A més és necessari configurar les següents propietats

Propietat Requerida Descripció
target Implementació del DAO sobre la que es definirà la transaccionalitat

Exemple:

...

<property name="dao" >

<bean  parent="baseDaoProxy">

<property name="target"  ref="productDaoTarget"/>

</bean>

</property>

...







Fitxer de configuració: action-servlet-XXX.xml
Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring

Cada acció que implementi una cerca necessita la definició d'un bean que hereti de la implementació DAO "pare" del servei (Spring amb Hibernate) i que tingui els següents atributs:
Atributs:
|| Atribut || Requerit || Descripció ||

class Implementació particular del DAO.

A més és necessari configurar les següents propietats

Propietat Requerida Descripció
sessionFactory Referència a la factoria de sessions d'Hibernate

Exemple:

...

<bean id="productDaoTarget"  class="net.gencat.ctti.canigo.samples.jpetstore.model.dao.
hibernate.impl.HibernateProductDAOImpl">
	<property  name="sessionFactory" ref="sessionFactory" />
</bean>

...



Utilització del Servei

Actors

En un escenari típic d'utilització de Canigó hi ha aspectes importants a considerar. En concret són els derivats de la relació dels nostres DAO's amb les 'BeanFactory' __que proporciona Spring i les 'SessionFactory' que proporciona Hibernate. Aquests aspectes són bàsicament tres:

1. D'on treiem les instàncies dels DAO's. Una vegada tenim els DAO's ja els podem començar a utilitzar a qualsevol classe. Se'ns ocòrrer, per exemple, una classe 'Action' d'Struts a on tinguem una variable d'instància que sigui un dels nostres DAO's i que guardi un objecte de negoci

...
public class CategoryAction extends ActionExtendedSupport {

    private CategoryDAO dao = null;
        ...
        public ActionForward save(Category vo, StrutsContext context) {
        ...
        dao.saveOrUpdate(vo);
        ...
        return context.getActionMapping().findForward("success");
        }

           ...
}


A l'exemple, la instància del CategoryDAO no l'hem creat nosaltres si no que és Spring qui la injecta al nostre 'Action'. Això és aplicable sempre i és l'escenari correcte d'utilització dels DAO's, és a dir, no serem nosaltres qui instanciaran els DAO's si no que deixarem a Spring aquesta tasca mitjançant els fitxers de configuració (típicament applicationContext.xml; a l'apartat Configuració s'explica com fer-ho). La següent figura mostra la jerarquia de dependències dels 'beans' d'Spring per aquest cas senzill (sense introduir cap concepte de transaccionalitat):

2. Com es relacionen els nostres DAO's amb la 'SessionFactory' d'Hibernate que han de fer servir. A la figura anterior veiem que el nostre DAO es relaciona amb la 'SessionFactory' d'Hibernate a partir de la propietat 'sessionFactory'. Això es així perquè els nostres DAO's hereten tots de 'net.gencat.ctti.canigo.services.persistence.spring.dao.impl.SpringHibernateDAOImpl'. Aquesta herència la definim a nivell de configuració de projecte d'Hibernate Synchronizer. A l'apartat Configuració s'explica com.



3. Com saben els nostres DAO's les característiques de transaccionalitat dels seus mètodes.
Aquest punt requereix un apartat separat que analitzem a continuació.

Transaccionalitat

L'escenari plantejat en l'apartat anterior es correspon amb un exemple senzill d'utilització d'un DAO qualsevol sense introduir en cap moment el concepte de transaccionalitat. Això no es correspon amb una aplicació complexe, on les operacions de base de dades es realitzen en bloc (transacció) i on el resultat pot ser que volguem fer enrera tota la transacció si es produeix un error.
Hem de definir, per tant, la transaccionalitat de les nostres operacions. Això implica, mirant l'exemple anterior, que el nostre dao.saveOrUpdate( ... ) ha de fer rollback o commit en funció de si es produeix o no un error. Així doncs, definirem que el nostre mètode en qüestió és transacional o no i quines característiques s'apliquen (segons s'ha introduït en l'apartat primer d'aquest document).

<bean id="categoryBOTarget" class="net.gencat.ctti.canigo.samples.prototip.model.bo.impl.CategoryBOImpl">
    <property name="dao" ref="universalHibernateDAO"/>
</bean>

<bean name="/categories" class="net.gencat.ctti.canigo.samples.prototip.struts.action.CategoryAction">
    ...
    <property name="bo" >
        <bean parent="baseDaoProxy">
            <property name="target">
                <ref bean="categoryBOTarget"/>
            </property>
        </bean>
    </property>
    ...
</bean>

El primer que destaca és l'aparició en escena d'un 'categoryDaoProxy' i un 'transactionManager'. El que abans era el 'categoryDao' ara és el 'categoryDaoTarget'. Ambdós beans són classes que ens proporciona Spring. L'explicació de com configurar-les es troba a l'apartat Configuració. La idea que hi ha darrere de tot això és ben senzilla. En lloc d'utilitzar directament el nostre DAO, utilitzem un proxy que ens intercepta les crides als nostres métodes i que afegeix la transaccionalitat amb el manager que li indiquem, en aquest cas un manager d'Hibernate que fa ús de la 'sessionFactory' configurada. A nivell de codi tot queda igual, és a dir, farem ús de les nostres interfícies DAO però per sota s'apliquen els conceptes necessaris per garantir la coherència de les nostres dades. Tot això és possible perquè és Spring qui ens proporciona les instàncies de les implementacions dels nostres DAO's, tal i com s'ha remarcat en l'apartat anterior.

Important! Amb la transaccionalitat d'Hibernate activada només es fa rollback al llançar una UncheckedException. En el cas que també es vulgui fer rollback amb les CheckedExceptions cal configurar-ho manualment afegint el següent codi "-java.lang.Throwable" al fitxer de configuració canigo-service-persistence.xml:

...
<property name="transactionAttributes">
	<props>
		<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
		<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
		<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
		<prop key="store*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop>
		<prop key="save*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop>
		<prop key="add*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop>
		<prop key="delete*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop>
	</props>
</property>
... 

Eines de Suport

Hibernate Tools

A l'hora de configurar la persistència, recomanem la utilització d'Hibernate Tools, que consisteix en un conjunt de plugins per a Eclipse que faciliten el treball amb Hibernate (mappings, HQL, ...). En aquest enllaç podeu trobar tota informació al respecte:

http://www.hibernate.org/255.html

Us podeu descarregar l'eina aquí

La utilització del servei de dades comporta el coneixement dels conceptes que es maneguen i els passos a seguir per a poder persistir les dades, més enllà de les classes disponibles en el propi framework (amb el suport de l'eina Hibernate Tools). Els passos a seguir en aquest ordre són:

  1. Creació dels fitxers propis d'Hibernate: hibernate.cfg.xml i fitxers de mappings (hbm.xml)
  2. Sincronització dels fitxers de mappings per a la creació dels DAO's i objectes de negoci.
  3. Definició del actors que intervenen en el servei de dades
  4. Definició de la transaccionalitat de les nostres operacions de negoci
  5. Utilització dels DAO's en la nostra aplicació

A continuació es detalla cadascun dels passos a seguir per a completar amb èxit una operació de persistència:

El primer que s'ha de fer és definir les propietats necessàries per al motor de persistència que farem servir: Hibernate 3. Típicament aquestes propietats es defineixen en un fitxer que es diu 'hibernate.cfg.xml'.
Per tant, en el menú 'File/New/Other' seleccionem 'Hibernate/Hibernate Configuration File'. Aquesta opció està disponible gràcies a que ens em instal.lat l'Hibernate Tools.
Se'ns obrirà una finestra on hem d'informar:

  1. Ubicació on situarem el fitxer que es crearà. Aquesta ubicació haurà de ser el directori de 'resources' del nostre projecte. Mitjançant el botó 'Browse' podem seleccionar aquesta destinació amb facilitat. Típicament serà 'project_name\src\main\resources\hibernate\config'
  2. File name: nom del fitxer. El valor per defecte, 'hibernate.cfg.xml', ja ens està bé.
  3. Session Factory Name: nom sota el que podem trobar la factoria de sessions hibernate en un arbre JNDI.
  4. Database Dialect: tipus de base de dades que farem servir.
  5. Driver Class: nom de la classe que fa de 'driver' per a la connexió amb la BD. Depèn de la BD que fem servir. Per exemple, per MySQL seria 'com.mysql.jdbc.Driver'. Mitjançant el botó 'Browse' podem localitzar amb facilitat aquesta classe. És important però que les llibreries que contenen els 'drivers' estiguin en el classpath del nostre projecte.
  6. Connection URL: cadena de connexió del a BD. Depèn de la BD que fem servir. Per exemple, per MySQL seria alguna cosa semblant a 'jdbc:mysql://localhost/test'
  7. Default Schema: esquema de base de dades que farem servir
  8. Default Catalog (no cal informar-lo)
  9. Username: usuari per la BD
  10. Password: contrasenya per la BD
  11. Seleccionar l'opció 'Create a console configuration'

Cliquem a 'Finish' i ja tenim el fitxer 'hibernate.cfg.xml' al directori que hem indicat:



Aquest és només el primer pas en la configuració del nostre motor de persistència. Realment falta afegir la informació rellevant: els 'mappings' dels nostres objectes de negoci. Cada taula presenta (en la majoria de casos) un objecte de negoci i té associat un fitxer de mapping. Aquest fitxer es diu igual que el nom de la taula (però amb la convenció Java) i té extensió 'hbm.xml'. Anem a crear doncs aquests fitxers. En el menú 'File/New/Other' seleccionem 'Hibernate/Hibernate Mapping File'. Aquesta opció està disponible gràcies a què ens em instal.lat l'Hibernate Tools. Se'ns obrirà una finestra on hem d'informar els següents camps:

  1. Ubicació on situarem els fitxers que es crearan. Aquesta ubicació haurà de ser el directori de 'resources' del nostre projecte i subdirectori hibernate el mateix on hem creat el fitxer 'hibernate.cfg.xml', en una subcarpeta que s'anomenara 'mappings'. És a dir, el directori en el qual desarem els fitxers de mappings serà doncs '/project_name/src/main/resources/hibernate/mappings'. Mitjançant el botó 'Browse' podem seleccionar aquesta destinació amb facilitat. Cal remarcar que aquest directori ha d'existir. Per tant, en el cas que no el tinguem, el crearem abans de crear els fitxers de mappings.
  2. FileName: nom del fitxer de mapping amb extensió '.hbm.xml'. Generalment farem servir el nom de la classe que volguem mapejar.
  3. Class to map: clase que fa referència al objecte (POJO) que volem mapejar amb la BD.

Hibernate Tools també ens ofereix la possibilitat de generar codi automàticament. Podeu consultar el seu funcionament descarregant-vos aquest fitxer.

Ara tenim, d'una banda els fitxers de mapeig i per l'altra el fitxer de configuració d'hibernate. Però com els lliguem? És tan fàcil com afegir els mappings al fitxer 'hibernate.cfg.xml' (Hibernate Tools dona suport a aquesta tasca)

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE  hibernate-configuration

PUBLIC "-//Hibernate/Hibernate Configuration  DTD//EN"

"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

...

<mapping  resource="test/Category.hbm.xml" />

<mapping  resource="test/Item.hbm.xml" />

<mapping  resource="test/Product.hbm.xml" />

<mapping  resource="test/Supplier.hbm.xml"  />

</session-factory>

</hibernate-configuration>


El següent pas en l'utilització del servei de dades és crear les següents classes:

  1. Interfícies DAO
  2. Implementacions DAO
  3. Objectes de negoci

Per a aquesta creació seleccionem els fitxers de mapeig i en el menú contextual seleccionem 'Hibernate Synchronizer/Sincronize Files':

Si tot va bé apareixan cinc directoris i l'estructura dels mateixos ha de ser alguna cosa similar a la figura següent:
On veiem:

  1. test: package dels objectes de negoci
  2. test.base: package base dels objectes de negoci
  3. test.dao: interfícies dels DAO's
  4. test.dao.hibernate.impl: implementacions dels DAO's
  5. test.dao.hibernate.impl.base: implementacions base dels DAO's

Qualsevol canvi a la base de dades només implicaria una resincronització dels fitxers de mapeig per tornar a generar les classes implicades amb l'opció 'Synchronize And Overwrite'. S'ha de tenir en compte que una resincronització fa que es sobreescriguin els fitxers que ja tenim amb els nous.

Exemples

Test unitaris

Com a exemple d'utilització implementarem un test unitari que faci recull de tots els conceptes implicats. Resumint, necessitem:

  1. Fitxer de configuració d'Hibernate (típicament 'hibernate.cfg.xml')
  2. Fitxers de mapeig de les taules que volem manegar (típicament '*.hbm.xml')
  3. Interfícies i implementacions dels DAO's.
  4. Objectes de negoci (també coneguts com 'Value Objects')
  5. Fitxer de configuració d'Spring (típicament applicationContext.xml)
  6. Classe principal. En el nostre cas extendrà TestCase i farà les vegades de 'Action' en una aplicació d'Struts.

Partim doncs d'una base de dades Hypersonic senzilla amb dues taules: CATEGORY i PRODUCT i una relació '1..n' entre Category i Product. També obviem la part de configuració de l'Hibernate Sync. ja que s'explica en detall en apartats posteriors.
Així doncs els passos són:

  1. Crear el fitxer de propietats d'Hibernate. Menú 'File/New/Other', seleccionem 'Hibernate/Hibernate Configuration File' i omplim totes les propietats correctament:

  2. Crear els directoris a on situarem els fitxers de mapeig: '/src/test/resources/com/myapp/model'

  3. Crear el fitxer de mapeig de les taules seleccionades. Menú 'File/New/Other', seleccionem 'Hibernate/Hibernate Mapping File' i omplim totes les propietats correctament:

  4. Sincronitzem els fitxers de mapeig per crear les classes Java i obtenim el següent resultat:

  5. Afegim el fitxer 'applicationContext.xml' al directori '/src/test/resources' per a configurar les nostres classes com a beans d'Spring. Farem servir un escenari transaccional per a veure tots els conceptes:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/
    spring-beans.dtd">
    
    <beans>
    <!-- Hibernate SessionFactory -->
    <bean name="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="configLocation" value="classpath:hibernate.cfg.xml" />
    </bean>
    
    <!-- Transaction manager for a single Hibernate SessionFactory (alternative to JTA) -->
    <bean id="transactionManager"
    class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
    </bean>
    
    <!-- Category DAO Proxy -->
    <bean id="categoryDaoProxy"
    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager">
    <ref local="transactionManager" />
    </property>
    <property name="target">
    <ref local="categoryDaoTarget" />
    </property>
    <property name="transactionAttributes">
    <props>
    <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
    <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
    <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
    <prop key="store*">PROPAGATION_REQUIRED</prop>
    <prop key="save*">PROPAGATION_REQUIRED</prop>
    </props>
    </property>
    </bean>
    <!-- Category DAO Target -->
    <bean id="categoryDaoTarget" class="com.myapp.model.dao.hibernate.impl.HibernateCategoryDAOImpl">
    <property name="sessionFactory" ref="sessionFactory" />
    </bean>
    <!-- Category Instances -->
    <bean id="category" class="com.myapp.model.Category" singleton="false"/>
    <!-- Product Instances -->
    <bean id="product" class="com.myapp.model.Product" singleton="false"/>
    <!-- Loggig Service -->
    <bean id="loggingConfigurator" class="net.gencat.ctti.canigo.services.logging.log4j.xml.
    HostDOMConfigurator" init-method="init">
    <property name="configFileName"><value>D:\users\manelix\workspace\canigo-services-persistence \src\
    test\resources\log4j-test.xml</value></property>
    </bean>
    <bean id="loggingService" class="net.gencat.ctti.canigo.services.logging.log4j.Log4JServiceImpl"
    init-method="init">
    <property name="configurator"><ref local="loggingConfigurator"/></property>
    </bean>
    </beans>


  6. Ara només queda implementar el TestCase.
    ...
    public class DAOTest extends TestCase {
        ...
        protected void setUp() throws Exception {
            super.setUp();
            ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext
            ("applicationContext.xml");
            this.factory = (BeanFactory) appContext;
            /**
            * Logging service
            */
            LoggingService logService = (LoggingService)factory.getBean("loggingService");
            this.log = logService.getLog(this.getClass());
        }
    
        public void testSave(){
            /**
            * Request the DAO to manipulate category instances
            */
            CategoryDAO categoryDAO = (CategoryDAO)factory.getBean("categoryDaoProxy");
            /**
            * Request a new category instance
            */
            Category newCategory = (Category)factory.getBean("category");
            newCategory.setName("name at "+System.currentTimeMillis());
            newCategory.setDescn("descn at "+System.currentTimeMillis());
            try {
                categoryDAO.save(newCategory);
            }
            catch(PersistenceServiceException ex){
                log.warn("Se ha producido un error: "+ex.getLocalizedMessage());
            }
            log.debug("El ID de la nueva category es: "+newCategory.getId());
            assertTrue("La category no se ha guardado correctamente!",newCategory.getId()!=null);
        }
    ...
    }


A l'exemple anterior cal destacar:

  1. La utilització dels DAO's al nostre codi és a través de les interfícies i no de les implementacions, encara que al fitxer de configuració d'Spring el bean retorna la implementació.
  2. Només hi ha una operació de base de dades. Com tot va bé s'executa un 'commit' totalment transparent per l'usuari que consolida les dades a la BD. Això poder no reflexa massa una transacció real, en la que en la mateixa transacció es poden executar múltiples operacions de BD. Anem per tant a crear una situació fictícia d'aquest comportament.
  1. Afegim un objecte fictici que faci vàries operacions de 'save' i que contingui el nostre DAO:
    <bean id="multiSaveTarget"  class="com.myapp.test.MultiSave">
    
    <property name="categoryDAO"  ref="categoryDaoProxy" />
    
    <property name="logService"  ref="loggingService" />
    
    </bean>


i afegim un gestor transaccional sobre aquest objecte per indicar que volem una transacció sobre el métode que inicia la transacció i llença totes les operacions de base de dades:

<bean id="multiSaveProxy"  class="org.springframework.transaction.interceptor.
TransactionProxyFactoryBean">

<property  name="transactionManager">

<ref local="transactionManager" />

</property>

<property name="target">

<ref local="multiSaveTarget" />

</property>

<property  name="transactionAttributes">

<props>

<prop key="save*">PROPAGATION_REQUIRED</prop>

<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>

</props>

</property>

</bean>


  1. Ara només hem de crear la classe 'MultiSave' :
    ...
    public class MultiSave {
        ...
        public void saveMultiple*(List categories) {
            Iterator it = categories*.iterator();
            int i = 0;
            while(it.hasNext())\{
                Category c = (Category)it.next();
                this.logService.getLog(this.getClass()).debug("Trying to save category...");
                if(i==categories.size()-1)\
                    this.categoryDAO.update(c);
                }
                else {
                    this.categoryDAO.save(c);
                }
                this.logService.getLog(this.getClass()).debug("Category saved with id "+
                c.getId());
                i++;
            }
        }
        ...
    }



i afegir un altre cas de test a la nostra classe DAOTest:

...
public class DAOTest extends TestCase {
    ...
    public void testMultipleSave(){
        /**
        * Request a the 'multisave object'
        */
        MultiSave multiSave = (MultiSave)factory.getBean("multiSaveProxy");
        ArrayList list = new ArrayList();
        for(int i=0;i<3;i++)\{
            /**
            * Request a new category instance
            */
            Category newCategory = (Category)factory.getBean("category");
            newCategory.setName("name at "+System.currentTimeMillis());
            newCategory.setDescn("descn at "+System.currentTimeMillis());
            list.add(newCategory);
        }
        try {
            multiSave.saveMultiple(list);
        }
        catch(PersistenceServiceException ex){
            log.warn("Se ha producido un error: "+ex.getLocalizedMessage());
        }
        Iterator it = list.iterator();
        while(it.hasNext()){
            Category c = (Category)it.next();
            if(c.getId()!=null){
                log.debug("Loading category with ID="+c.getId());
                Category cLoaded = multiSave*.loadCategory( c.getId() );
                log.debug("Loaded from BD a category "cLoaded" with Id="c.getId()"?"+(cLoaded!=null));
                assertTrue("The category has been saved into DB and should not!",cLoaded==null);
            }
        }
        log.debug("End.");
    }
    ...
}


Si ens fixem, en el mètode saveMultiple( ... ) el que estem fent és provocar un error a l'hora de guardar la última Category, ja que estem cridant a update( ... ) quan la instància és nova i hauríem de cridar a save( ... ). Això provocarà un 'rollback' de totes les instàncies previament guardades. Com ho comprovem? Posteriorment en el codi intentem fem un load( ... ) per veure si s'ha desat algun registre. Això mateix ho podem mirar veient els logs generats:

DEBUG [main] org.hibernate.transaction.JDBCTransaction - begin
DEBUG [main] org.hibernate.transaction.JDBCTransaction - current autocommit status: false
DEBUG [main] com.myapp.test.MultiSave - Trying to save category...
DEBUG [main] com.myapp.test.MultiSave - Category saved with id 1
DEBUG [main] com.myapp.test.MultiSave - Trying to save category...
DEBUG [main] com.myapp.test.MultiSave - Category saved with id 2
DEBUG [main] com.myapp.test.MultiSave - Trying to save category...
DEBUG [main] com.myapp.test.MultiSave - Persistence error will occur...
DEBUG [main] org.hibernate.transaction.JDBCTransaction - rollback
DEBUG [main] org.hibernate.transaction.JDBCTransaction - rolled back JDBC Connection
WARN [main] com.myapp.test.DAOTest - Se ha producido un error: org.springframework.dao.InvalidDataAccessApiUsageException: The given object has a null identifier: com.myapp.model.Category; nested exception is org.hibernate.TransientObjectException: The given object has a null identifier: com.myapp.model.Category
DEBUG [main] com.myapp.test.DAOTest - Loading category with ID=1
DEBUG [main] org.hibernate.transaction.JDBCTransaction - begin
DEBUG [main] org.hibernate.transaction.JDBCTransaction - current autocommit status: false
DEBUG [main] org.hibernate.transaction.JDBCTransaction - commit
DEBUG [main] org.hibernate.transaction.JDBCTransaction - committed JDBC Connection
DEBUG [main] com.myapp.test.DAOTest - Loaded from BD a category null with Id=1?false
DEBUG [main] com.myapp.test.DAOTest - Loading category with ID=2
DEBUG [main] org.hibernate.transaction.JDBCTransaction - begin
DEBUG [main] org.hibernate.transaction.JDBCTransaction - current autocommit status: false
DEBUG [main] org.hibernate.transaction.JDBCTransaction - commit
DEBUG [main] org.hibernate.transaction.JDBCTransaction - committed JDBC Connection
DEBUG [main] com.myapp.test.DAOTest - Loaded from BD a category null with Id=2?false
DEBUG [main] com.myapp.test.DAOTest - End.

La seqüència és la següent:

  1. S'obre una transacció (missatge: org.hibernate.transaction.JDBCTransaction - begin)
  2. S'intenten desar les dues primeres 'Category'. Tot correcte
  3. S'intenta actualitzar la tercera 'Category'. Error. El framework fa un rollback de tot i no es desa res (missatge: org.hibernate.transaction.JDBCTransaction - rollback). Tanquem transacció.
  4. Obrim transacció (missatge: org.hibernate.transaction.JDBCTransaction - begin)
  5. Intentem carregar de la base de dades els suposats registres que s'havien desat sense problemes (ID=1 i ID=2; missatge: Loading category with ID=1 i ID=2)
  6. Tanquem transacció (missatge: org.hibernate.transaction.JDBCTransaction - commit)
  7. Les dades no existeixen. Tot és correcte (missatge: Loaded from BD a category null with Id=1 i Id=2?false)